/*-------------------------------------------------------------------------
 *
 * gistbuildbuffers.c
 *	  GiST缓冲构建算法的节点缓冲区管理功能。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/gist/gistbuildbuffers.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/gist_private.h"
#include "catalog/index.h"
#include "miscadmin.h"
#include "storage/buffile.h"
#include "storage/bufmgr.h"
#include "utils/memutils.h"
#include "utils/rel.h"

static GISTNodeBufferPage *fc_gistAllocateNewPageBuffer(GISTBuildBuffers *fc_gfbb);
static void fc_gistAddLoadedBuffer(GISTBuildBuffers *fc_gfbb,
								GISTNodeBuffer *fc_nodeBuffer);
static void fc_gistLoadNodeBuffer(GISTBuildBuffers *fc_gfbb,
							   GISTNodeBuffer *fc_nodeBuffer);
static void fc_gistUnloadNodeBuffer(GISTBuildBuffers *fc_gfbb,
								 GISTNodeBuffer *fc_nodeBuffer);
static void fc_gistPlaceItupToPage(GISTNodeBufferPage *fc_pageBuffer,
								IndexTuple fc_item);
static void fc_gistGetItupFromPage(GISTNodeBufferPage *fc_pageBuffer,
								IndexTuple *fc_item);
static long fc_gistBuffersGetFreeBlock(GISTBuildBuffers *fc_gfbb);
static void fc_gistBuffersReleaseBlock(GISTBuildBuffers *fc_gfbb, long fc_blocknum);

static void fc_ReadTempFileBlock(BufFile *fc_file, long fc_blknum, void *fc_ptr);
static void fc_WriteTempFileBlock(BufFile *fc_file, long fc_blknum, void *fc_ptr);


/*
 * 初始化 GiST 构建缓冲区。
 */
GISTBuildBuffers *
gistInitBuildBuffers(int fc_pagesPerBuffer, int fc_levelStep, int fc_maxLevel)
{
	GISTBuildBuffers *fc_gfbb;
	HASHCTL		fc_hashCtl;

	fc_gfbb = palloc(sizeof(GISTBuildBuffers));
	fc_gfbb->pagesPerBuffer = fc_pagesPerBuffer;
	fc_gfbb->levelStep = fc_levelStep;

	/*
	 * 创建一个临时文件，用于存放从内存中交换出去的缓冲页面。
	 */
	fc_gfbb->pfile = BufFileCreateTemp(false);
	fc_gfbb->nFileBlocks = 0;

	/* 初始化空闲页面管理。 */
	fc_gfbb->nFreeBlocks = 0;
	fc_gfbb->freeBlocksLen = 32;
	fc_gfbb->freeBlocks = (long *) palloc(fc_gfbb->freeBlocksLen * sizeof(long));

	/*
	 * 当前内存上下文将被用于所有在缓冲构建期间持续存在的内存数据结构的缓冲区。
	 */
	fc_gfbb->context = CurrentMemoryContext;

	/*
	 * nodeBuffersTab 哈希表是索引块与其缓冲区之间的关联。
	 */
	fc_hashCtl.keysize = sizeof(BlockNumber);
	fc_hashCtl.entrysize = sizeof(GISTNodeBuffer);
	fc_hashCtl.hcxt = CurrentMemoryContext;
	fc_gfbb->nodeBuffersTab = hash_create("gistbuildbuffers",
									   1024,
									   &fc_hashCtl,
									   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	fc_gfbb->bufferEmptyingQueue = NIL;

	/*
	 * 每级节点缓冲区列表用于最终缓冲区清空过程。在创建时，节点缓冲区会被插入到这里。
	 */
	fc_gfbb->buffersOnLevelsLen = 1;
	fc_gfbb->buffersOnLevels = (List **) palloc(sizeof(List *) *
											 fc_gfbb->buffersOnLevelsLen);
	fc_gfbb->buffersOnLevels[0] = NIL;

	/*
	 * 节点缓冲区的块编号，其最后页面当前已加载到主内存中。
	 */
	fc_gfbb->loadedBuffersLen = 32;
	fc_gfbb->loadedBuffers = (GISTNodeBuffer **) palloc(fc_gfbb->loadedBuffersLen *
													 sizeof(GISTNodeBuffer *));
	fc_gfbb->loadedBuffersCount = 0;

	fc_gfbb->rootlevel = fc_maxLevel;

	return fc_gfbb;
}

/*
 * 返回给定块的节点缓冲区。如果它尚不存在，则创建该缓冲区。
 */
GISTNodeBuffer *
gistGetNodeBuffer(GISTBuildBuffers *fc_gfbb, GISTSTATE *fc_giststate,
				  BlockNumber fc_nodeBlocknum, int fc_level)
{
	GISTNodeBuffer *fc_nodeBuffer;
	bool		fc_found;

	/* 在哈希表中查找节点缓冲区 */
	fc_nodeBuffer = (GISTNodeBuffer *) hash_search(fc_gfbb->nodeBuffersTab,
												(const void *) &fc_nodeBlocknum,
												HASH_ENTER,
												&fc_found);
	if (!fc_found)
	{
		/*
		 * 未找到节点缓冲区。将新缓冲区初始化为空。
		 */
		MemoryContext fc_oldcxt = MemoryContextSwitchTo(fc_gfbb->context);

		/* nodeBuffer->nodeBlocknum 是哈希键，已填充 */
		fc_nodeBuffer->blocksCount = 0;
		fc_nodeBuffer->pageBlocknum = InvalidBlockNumber;
		fc_nodeBuffer->pageBuffer = NULL;
		fc_nodeBuffer->queuedForEmptying = false;
		fc_nodeBuffer->isTemp = false;
		fc_nodeBuffer->level = fc_level;

		/*
		 * 将此缓冲区添加到该级别的缓冲区列表中。如有必要，扩大 buffersOnLevels 数组。
		 */
		if (fc_level >= fc_gfbb->buffersOnLevelsLen)
		{
			int			fc_i;

			fc_gfbb->buffersOnLevels =
				(List **) repalloc(fc_gfbb->buffersOnLevels,
								   (fc_level + 1) * sizeof(List *));

			/* 初始化扩展部分 */
			for (fc_i = fc_gfbb->buffersOnLevelsLen; fc_i <= fc_level; fc_i++)
				fc_gfbb->buffersOnLevels[fc_i] = NIL;
			fc_gfbb->buffersOnLevelsLen = fc_level + 1;
		}

		/*
		 * 将新缓冲区添加到该级别的缓冲区列表的开头。新缓冲区放在列表的开头并不是任意的：在最终清空阶段，我们会遍历每个级别的所有缓冲区，并刷新它们。如果在清空过程中发生页面分割，首先刷新新分割出的页面比继续处理该级别上已存在的页面更有效率。在页面分割过程中刚创建的缓冲区很可能仍在缓存中，因此立即刷新它们比将它们放到队列的末尾更有效。
		 */
		fc_gfbb->buffersOnLevels[fc_level] = lcons(fc_nodeBuffer,
											 fc_gfbb->buffersOnLevels[fc_level]);

		MemoryContextSwitchTo(fc_oldcxt);
	}

	return fc_nodeBuffer;
}

/*
 * 为缓冲页面分配内存。
 */
static GISTNodeBufferPage *
fc_gistAllocateNewPageBuffer(GISTBuildBuffers *fc_gfbb)
{
	GISTNodeBufferPage *fc_pageBuffer;

	fc_pageBuffer = (GISTNodeBufferPage *) MemoryContextAllocZero(fc_gfbb->context,
															   BLCKSZ);
	fc_pageBuffer->prev = InvalidBlockNumber;

	/* 设置页面的空闲空间 */
	PAGE_FREE_SPACE(fc_pageBuffer) = BLCKSZ - BUFFER_PAGE_DATA_OFFSET;
	return fc_pageBuffer;
}

/*
 * 将指定的缓冲区添加到 loadedBuffers 数组中。
 */
static void fc_gistAddLoadedBuffer(GISTBuildBuffers *fc_gfbb, GISTNodeBuffer *fc_nodeBuffer)
{
	/* 永远不要将临时缓冲区添加到数组中 */
	if (fc_nodeBuffer->isTemp)
		return;

	/* 如有必要，扩大数组 */
	if (fc_gfbb->loadedBuffersCount >= fc_gfbb->loadedBuffersLen)
	{
		fc_gfbb->loadedBuffersLen *= 2;
		fc_gfbb->loadedBuffers = (GISTNodeBuffer **)
			repalloc(fc_gfbb->loadedBuffers,
					 fc_gfbb->loadedBuffersLen * sizeof(GISTNodeBuffer *));
	}

	fc_gfbb->loadedBuffers[fc_gfbb->loadedBuffersCount] = fc_nodeBuffer;
	fc_gfbb->loadedBuffersCount++;
}

/*
 * 将节点缓冲区的最后页面加载到主内存中。
 */
static void fc_gistLoadNodeBuffer(GISTBuildBuffers *fc_gfbb, GISTNodeBuffer *fc_nodeBuffer)
{
	/* 检查我们是否真的应该加载一些内容 */
	if (!fc_nodeBuffer->pageBuffer && fc_nodeBuffer->blocksCount > 0)
	{
		/* 为页面分配内存 */
		fc_nodeBuffer->pageBuffer = fc_gistAllocateNewPageBuffer(fc_gfbb);

		/* 从临时文件中读取块 */
		fc_ReadTempFileBlock(fc_gfbb->pfile, fc_nodeBuffer->pageBlocknum,
						  fc_nodeBuffer->pageBuffer);

		/* 将文件块标记为可用 */
		fc_gistBuffersReleaseBlock(fc_gfbb, fc_nodeBuffer->pageBlocknum);

		/* 将节点缓冲区标记为已加载 */
		fc_gistAddLoadedBuffer(fc_gfbb, fc_nodeBuffer);
		fc_nodeBuffer->pageBlocknum = InvalidBlockNumber;
	}
}

/*
 * 将节点缓冲区的最后页面写入磁盘。
 */
static void fc_gistUnloadNodeBuffer(GISTBuildBuffers *fc_gfbb, GISTNodeBuffer *fc_nodeBuffer)
{
	/* 检查我们是否有东西可以写 */
	if (fc_nodeBuffer->pageBuffer)
	{
		BlockNumber fc_blkno;

		/* 获取空闲文件块 */
		fc_blkno = fc_gistBuffersGetFreeBlock(fc_gfbb);

		/* 将块写入临时文件 */
		fc_WriteTempFileBlock(fc_gfbb->pfile, fc_blkno, fc_nodeBuffer->pageBuffer);

		/* 释放该页面的内存 */
		pfree(fc_nodeBuffer->pageBuffer);
		fc_nodeBuffer->pageBuffer = NULL;

		/* 保存块编号 */
		fc_nodeBuffer->pageBlocknum = fc_blkno;
	}
}

/*
 * 将所有节点缓冲区的最后页面写入磁盘。
 */
void gistUnloadNodeBuffers(GISTBuildBuffers *fc_gfbb)
{
	int			fc_i;

	/* 卸载所有在内存中加载了页面的缓冲区。 */
	for (fc_i = 0; fc_i < fc_gfbb->loadedBuffersCount; fc_i++)
		fc_gistUnloadNodeBuffer(fc_gfbb, fc_gfbb->loadedBuffers[fc_i]);

	/* 现在没有节点缓冲区加载了最后页面 */
	fc_gfbb->loadedBuffersCount = 0;
}

/*
 * 将索引元组添加到缓冲页面。
 */
static void fc_gistPlaceItupToPage(GISTNodeBufferPage *fc_pageBuffer, IndexTuple fc_itup)
{
	Size		fc_itupsz = IndexTupleSize(fc_itup);
	char	   *fc_ptr;

	/* 这里应该有足够的空间。 */
	Assert(PAGE_FREE_SPACE(fc_pageBuffer) >= MAXALIGN(fc_itupsz));

	/* 将页面的空闲空间值减少，以保留一个位置给元组。 */
	PAGE_FREE_SPACE(fc_pageBuffer) -= MAXALIGN(fc_itupsz);

	/* 获取我们保留的位置信息（即，空闲空间的末尾）。 */
	fc_ptr = (char *) fc_pageBuffer + BUFFER_PAGE_DATA_OFFSET
		+ PAGE_FREE_SPACE(fc_pageBuffer);

	/* 将索引元组复制到那里。 */
	memcpy(fc_ptr, fc_itup, fc_itupsz);
}

/*
 * 从缓冲区页面获取最后一个项目并将其从页面中删除。
 */
static void fc_gistGetItupFromPage(GISTNodeBufferPage *fc_pageBuffer, IndexTuple *fc_itup)
{
	IndexTuple	fc_ptr;
	Size		fc_itupsz;

	Assert(!PAGE_IS_EMPTY(fc_pageBuffer)); /* 页面不应该为空 */

	/* 获取最后一个索引元组的指针 */
	fc_ptr = (IndexTuple) ((char *) fc_pageBuffer
						+ BUFFER_PAGE_DATA_OFFSET
						+ PAGE_FREE_SPACE(fc_pageBuffer));
	fc_itupsz = IndexTupleSize(fc_ptr);

	/* 复制元组 */
	*fc_itup = (IndexTuple) palloc(fc_itupsz);
	memcpy(*fc_itup, fc_ptr, fc_itupsz);

	/* 将元组所占用的空间标记为可用 */
	PAGE_FREE_SPACE(fc_pageBuffer) += MAXALIGN(fc_itupsz);
}

/*
 * 将索引元组推送到节点缓冲区。
 */
void gistPushItupToNodeBuffer(GISTBuildBuffers *fc_gfbb, GISTNodeBuffer *fc_nodeBuffer,
						 IndexTuple fc_itup)
{
	/*
	 * 大部分内存操作将会在缓冲构建持久化上下文中进行。
	 * 所以，让我们切换到它。
	 */
	MemoryContext fc_oldcxt = MemoryContextSwitchTo(fc_gfbb->context);

	/*
	 * 如果缓冲区当前为空，创建第一页。
	 */
	if (fc_nodeBuffer->blocksCount == 0)
	{
		fc_nodeBuffer->pageBuffer = fc_gistAllocateNewPageBuffer(fc_gfbb);
		fc_nodeBuffer->blocksCount = 1;
		fc_gistAddLoadedBuffer(fc_gfbb, fc_nodeBuffer);
	}

	/* 如果最后一页的节点缓冲区不在内存中，加载最后一页 */
	if (!fc_nodeBuffer->pageBuffer)
		fc_gistLoadNodeBuffer(fc_gfbb, fc_nodeBuffer);

	/*
	 * 检查最后一页是否有足够的空间存放元组。
	 */
	if (PAGE_NO_SPACE(fc_nodeBuffer->pageBuffer, fc_itup))
	{
		/*
		 * 不。将前一个块交换到磁盘并分配一个新的块。
		 */
		BlockNumber fc_blkno;

		/* 将填充的页面写入磁盘 */
		fc_blkno = fc_gistBuffersGetFreeBlock(fc_gfbb);
		fc_WriteTempFileBlock(fc_gfbb->pfile, fc_blkno, fc_nodeBuffer->pageBuffer);

		/*
		 * 将内存中的页面重置为空，并通过在prev-link中存储其块编号
		 * 将前一个块链接到新页面。
		 */
		PAGE_FREE_SPACE(fc_nodeBuffer->pageBuffer) =
			BLCKSZ - MAXALIGN(offsetof(GISTNodeBufferPage, tupledata));
		fc_nodeBuffer->pageBuffer->prev = fc_blkno;

		/* 我们刚刚添加了一个页面 */
		fc_nodeBuffer->blocksCount++;
	}

	fc_gistPlaceItupToPage(fc_nodeBuffer->pageBuffer, fc_itup);

	/*
	 * 如果缓冲区刚刚溢出，将其添加到清空队列。
	 */
	if (BUFFER_HALF_FILLED(fc_nodeBuffer, fc_gfbb) && !fc_nodeBuffer->queuedForEmptying)
	{
		fc_gfbb->bufferEmptyingQueue = lcons(fc_nodeBuffer,
										  fc_gfbb->bufferEmptyingQueue);
		fc_nodeBuffer->queuedForEmptying = true;
	}

	/* 恢复内存上下文 */
	MemoryContextSwitchTo(fc_oldcxt);
}

/*
 * 从节点缓冲区中移除一个索引元组。如果成功返回true，节点缓冲区为空时返回false。
 */
bool gistPopItupFromNodeBuffer(GISTBuildBuffers *fc_gfbb, GISTNodeBuffer *fc_nodeBuffer,
						  IndexTuple *fc_itup)
{
	/*
	 * 如果节点缓冲区为空，则返回false。
	 */
	if (fc_nodeBuffer->blocksCount <= 0)
		return false;

	/* 如果需要，加载节点缓冲区的最后一页 */
	if (!fc_nodeBuffer->pageBuffer)
		fc_gistLoadNodeBuffer(fc_gfbb, fc_nodeBuffer);

	/*
	 * 从最后一个非空页面获取索引元组。
	 */
	fc_gistGetItupFromPage(fc_nodeBuffer->pageBuffer, fc_itup);

	/*
	 * 如果我们刚刚从页面中移除了最后一个元组，则在该节点缓冲区上抓取前一个页面（如果有）。
	 */
	if (PAGE_IS_EMPTY(fc_nodeBuffer->pageBuffer))
	{
		BlockNumber fc_prevblkno;

		/*
		 * blocksCount包括pageBuffer中的页面，所以现在减少它。
		 */
		fc_nodeBuffer->blocksCount--;

		/*
		 * 如果还有更多页面，抓取前一个。
		 */
		fc_prevblkno = fc_nodeBuffer->pageBuffer->prev;
		if (fc_prevblkno != InvalidBlockNumber)
		{
			/* 存在前一个页面。抓取它。 */
			Assert(fc_nodeBuffer->blocksCount > 0);
			fc_ReadTempFileBlock(fc_gfbb->pfile, fc_prevblkno, fc_nodeBuffer->pageBuffer);

			/*
			 * 现在我们已经将块读取到内存中，可以释放其在磁盘上的块以供重用。
			 */
			fc_gistBuffersReleaseBlock(fc_gfbb, fc_prevblkno);
		}
		else
		{
			/* 没有更多页面。释放内存。 */
			Assert(fc_nodeBuffer->blocksCount == 0);
			pfree(fc_nodeBuffer->pageBuffer);
			fc_nodeBuffer->pageBuffer = NULL;
		}
	}
	return true;
}

/*
 * 选择一个当前未使用的块进行写入。
 */
static long fc_gistBuffersGetFreeBlock(GISTBuildBuffers *fc_gfbb)
{
	/*
	 * 如果有多个空闲块，我们选择在freeBlocks[]中最后出现的那个。
	 * 如果没有，分配文件末尾的下一个块（导致文件扩展）。
	 */
	if (fc_gfbb->nFreeBlocks > 0)
		return fc_gfbb->freeBlocks[--fc_gfbb->nFreeBlocks];
	else
		return fc_gfbb->nFileBlocks++;
}

/*
 * 将块编号返回到空闲列表。
 */
static void fc_gistBuffersReleaseBlock(GISTBuildBuffers *fc_gfbb, long fc_blocknum)
{
	int			fc_ndx;

	/* 如果满了，则扩大freeBlocks数组。 */
	if (fc_gfbb->nFreeBlocks >= fc_gfbb->freeBlocksLen)
	{
		fc_gfbb->freeBlocksLen *= 2;
		fc_gfbb->freeBlocks = (long *) repalloc(fc_gfbb->freeBlocks,
											 fc_gfbb->freeBlocksLen *
											 sizeof(long));
	}

	/* 将块编号添加到数组 */
	fc_ndx = fc_gfbb->nFreeBlocks++;
	fc_gfbb->freeBlocks[fc_ndx] = fc_blocknum;
}

/*
 * 释放缓冲构建数据结构。
 */
void gistFreeBuildBuffers(GISTBuildBuffers *fc_gfbb)
{
	/* 关闭缓冲文件。 */
	BufFileClose(fc_gfbb->pfile);

	/* 其他所有内容将在释放内存上下文时释放 */
}

/*
 * 表示有关节点缓冲区的信息的数据结构，用于从拆分的节点缓冲区
 * 重新定位索引元组。
 */
typedef struct
{
	GISTENTRY	entry[INDEX_MAX_KEYS];
	bool		isnull[INDEX_MAX_KEYS];
	GISTPageSplitInfo *splitinfo;
	GISTNodeBuffer *nodeBuffer;
} RelocationBufferInfo;

/*
 * 在页面拆分时，将来自拆分页面的缓冲区的元组分配到为创建的页面
 * 一半而创建的新缓冲区。这还调整'splitinfo'中的下行链接
 * 以包括缓冲区中的元组。
 */
void gistRelocateBuildBuffersOnSplit(GISTBuildBuffers *fc_gfbb, GISTSTATE *fc_giststate,
								Relation fc_r, int fc_level,
								Buffer fc_buffer, List *fc_splitinfo)
{
	RelocationBufferInfo *fc_relocationBuffersInfos;
	bool		fc_found;
	GISTNodeBuffer *fc_nodeBuffer;
	BlockNumber fc_blocknum;
	IndexTuple	fc_itup;
	int			fc_splitPagesCount = 0,
				fc_i;
	GISTENTRY	fc_entry[INDEX_MAX_KEYS];
	bool		fc_isnull[INDEX_MAX_KEYS];
	GISTNodeBuffer fc_oldBuf;
	ListCell   *fc_lc;

	/* 如果拆分页面没有缓冲区，我们无事可做。 */
	if (!LEVEL_HAS_BUFFERS(fc_level, fc_gfbb))
		return;

	/*
	 * 获取拆分页面的节点缓冲区。
	 */
	fc_blocknum = BufferGetBlockNumber(fc_buffer);
	fc_nodeBuffer = hash_search(fc_gfbb->nodeBuffersTab, &fc_blocknum,
							 HASH_FIND, &fc_found);
	if (!fc_found)
	{
		/* 页面没有缓冲区，所以我们无事可做。 */
		return;
	}

	/*
	 * 复制旧的缓冲区，因为我们将把它重新用作新左页的缓冲区，
	 * 该页与旧页在同一个块上。根页并不符合这一点，但没关系，
	 * 因为我们在根页上从来没有缓冲区。Arge 等人描述的原始算法
	 * 确实如此，但没有用处，因为您不如直接从堆中读取元组
	 * 而不是从根缓冲区读取。
	 */
	Assert(fc_blocknum != GIST_ROOT_BLKNO);
	memcpy(&fc_oldBuf, fc_nodeBuffer, sizeof(GISTNodeBuffer));
	fc_oldBuf.isTemp = true;

	/* 重置旧缓冲区，从现在开始用于新左页 */
	fc_nodeBuffer->blocksCount = 0;
	fc_nodeBuffer->pageBuffer = NULL;
	fc_nodeBuffer->pageBlocknum = InvalidBlockNumber;

	/*
	 * 为重定位缓冲区的信息分配内存。
	 */
	fc_splitPagesCount = list_length(fc_splitinfo);
	fc_relocationBuffersInfos =
		(RelocationBufferInfo *) palloc(sizeof(RelocationBufferInfo) *
										fc_splitPagesCount);

	/*
	 * 填写通过分裂产生的页的节点缓冲区的重定位缓冲区信息。
	 */
	fc_i = 0;
	foreach(fc_lc, fc_splitinfo)
	{
		GISTPageSplitInfo *fc_si = (GISTPageSplitInfo *) lfirst(fc_lc);
		GISTNodeBuffer *fc_newNodeBuffer;

		/* 解压缩节点缓冲区页的父索引元组。 */
		gistDeCompressAtt(fc_giststate, fc_r,
						  fc_si->downlink, NULL, (OffsetNumber) 0,
						  fc_relocationBuffersInfos[fc_i].entry,
						  fc_relocationBuffersInfos[fc_i].isnull);

		/*
		 * 为该页创建一个节点缓冲区。最左侧的一半与分裂前的旧页
		 * 在同一个块上，因此对于最左侧的一半，这将返回原始缓冲区。
		 * 原始缓冲区中的元组已重新链接到临时缓冲区，因此
		 * 原始缓冲区现在为空。
		 */
		fc_newNodeBuffer = gistGetNodeBuffer(fc_gfbb, fc_giststate, BufferGetBlockNumber(fc_si->buf), fc_level);

		fc_relocationBuffersInfos[fc_i].nodeBuffer = fc_newNodeBuffer;
		fc_relocationBuffersInfos[fc_i].splitinfo = fc_si;

		fc_i++;
	}

	/*
	 * 遍历正在分裂的页的缓冲区中的所有索引元组，
	 * 将它们移动到新页的缓冲区。我们尝试将每个元组移动到
	 * 会导致前导列最低惩罚的页面，或者在平局的情况下，
	 * 在没有平局的情况下，最低惩罚的最早列。
	 *
	 * 页面搜索逻辑与 gistchoose() 非常相似。
	 */
	while (gistPopItupFromNodeBuffer(fc_gfbb, &fc_oldBuf, &fc_itup))
	{
		float		fc_best_penalty[INDEX_MAX_KEYS];
		int			fc_i,
					fc_which;
		IndexTuple	fc_newtup;
		RelocationBufferInfo *fc_targetBufferInfo;

		gistDeCompressAtt(fc_giststate, fc_r,
						  fc_itup, NULL, (OffsetNumber) 0, fc_entry, fc_isnull);

		/* 默认使用第一页（应该没关系） */
		fc_which = 0;

		/*
		 * best_penalty[j] 是到目前为止我们看到的列
		 * j 的最佳惩罚，或 -1 当我们尚未检查列 j。
		 * 第一个 -1 右侧的数组条目未定义。
		 */
		fc_best_penalty[0] = -1;

		/*
		 * 遍历可能的目标页，寻找将此元组移动到的页。
		 */
		for (fc_i = 0; fc_i < fc_splitPagesCount; fc_i++)
		{
			RelocationBufferInfo *fc_splitPageInfo = &fc_relocationBuffersInfos[fc_i];
			bool		fc_zero_penalty;
			int			fc_j;

			fc_zero_penalty = true;

			/* 遍历索引属性。 */
			for (fc_j = 0; fc_j < IndexRelationGetNumberOfKeyAttributes(fc_r); fc_j++)
			{
				float		fc_usize;

				/* 计算此列的惩罚。 */
				fc_usize = gistpenalty(fc_giststate, fc_j,
									&fc_splitPageInfo->entry[fc_j],
									fc_splitPageInfo->isnull[fc_j],
									&fc_entry[fc_j], fc_isnull[fc_j]);
				if (fc_usize > 0)
					fc_zero_penalty = false;

				if (fc_best_penalty[fc_j] < 0 || fc_usize < fc_best_penalty[fc_j])
				{
					/*
					 * 此列的新最佳惩罚。暂时选择此
					 * 页作为目标，并记录最佳惩罚。然后
					 * 将下一个列的惩罚重置为“未知”（间接
					 * 地，将右侧所有列的惩罚也重置为“未知”）。
					 * 这将迫使我们在后续循环迭代中
					 * 将此页的惩罚值作为所有剩余列的最佳值。
					 */
					fc_which = fc_i;
					fc_best_penalty[fc_j] = fc_usize;

					if (fc_j < IndexRelationGetNumberOfKeyAttributes(fc_r) - 1)
						fc_best_penalty[fc_j + 1] = -1;
				}
				else if (fc_best_penalty[fc_j] == fc_usize)
				{
					/*
					 * 当前页对于此列的效果与目前为止最好的
					 * 页相同。此循环的下一次迭代将比较
					 * 下一个列。
					 */
				}
				else
				{
					/*
					 * 当前页对于此列的效果比目前为止最好的
					 * 页差。跳过剩余列并继续检查
					 * 下一个页，如果有的话。
					 */
					fc_zero_penalty = false;	/* 以便外部循环不会退出 */
					break;
				}
			}

			/*
			 * 如果我们找到一个对所有列惩罚为零的页，
			 * 就没有必要检查剩余页；只需跳出循环并
			 * 返回该页。
			 */
			if (fc_zero_penalty)
				break;
		}

		/* 好的，“哪个”是将元组推送到的页索引 */
		fc_targetBufferInfo = &fc_relocationBuffersInfos[fc_which];

		/* 将项目推送到选定的节点缓冲区 */
		gistPushItupToNodeBuffer(fc_gfbb, fc_targetBufferInfo->nodeBuffer, fc_itup);

		/* 如果需要，调整此页面的下行链路。 */
		fc_newtup = gistgetadjusted(fc_r, fc_targetBufferInfo->splitinfo->downlink,
								 fc_itup, fc_giststate);
		if (fc_newtup)
		{
			gistDeCompressAtt(fc_giststate, fc_r,
							  fc_newtup, NULL, (OffsetNumber) 0,
							  fc_targetBufferInfo->entry,
							  fc_targetBufferInfo->isnull);

			fc_targetBufferInfo->splitinfo->downlink = fc_newtup;
		}
	}

	pfree(fc_relocationBuffersInfos);
}


/*
 * BufFile操作的包装器。主要区别在于这些
 * 包装器通过ereport()报告错误，因此调用者不需要
 * 检查返回码。
 */

static void fc_ReadTempFileBlock(BufFile *fc_file, long fc_blknum, void *fc_ptr)
{
	size_t		fc_nread;

	if (BufFileSeekBlock(fc_file, fc_blknum) != 0)
		elog(ERROR, "could not seek to block %ld in temporary file", fc_blknum);
	fc_nread = BufFileRead(fc_file, fc_ptr, BLCKSZ);
	if (fc_nread != BLCKSZ)
		elog(ERROR, "could not read temporary file: read only %zu of %zu bytes",
			 fc_nread, (size_t) BLCKSZ);
}

static void fc_WriteTempFileBlock(BufFile *fc_file, long fc_blknum, void *fc_ptr)
{
	if (BufFileSeekBlock(fc_file, fc_blknum) != 0)
		elog(ERROR, "could not seek to block %ld in temporary file", fc_blknum);
	BufFileWrite(fc_file, fc_ptr, BLCKSZ);
}
